In [2]:
import plotly.io as pio
pio.renderers.default = 'notebook_connected'

스타벅스 매장 입지 분석

서울시 내 출범한 스타벅스의 위치 및 인구통계 데이터를 통해,

  1. 거주인구가 많은 지역에 스타벅스 매장이 많이 입지해 있을 것이다.
  2. 직장인이 많은 지역에 스타벅스 매장이 많이 입지해 있을 것이다.

위의 가설들을 확인해보려 함

1. 데이터 수집

1.1 서울시 스타벅스 매장 목록 데이터

In [3]:
# 필요한 라이브러리 임포팅
from selenium import webdriver
from bs4 import BeautifulSoup
import pandas as pd
import time
In [7]:
# 스타벅스 매장 정보 접속
browser = webdriver.Chrome("C:/Users/DooDoo/Documents/data_analysis/chromedriver.exe")
url = "https://www.starbucks.co.kr/store/store_map.do?disp=locale"
browser.get(url)
time.sleep(3)

# 서울에 있는 전체 매장 클릭
seoul_btn = "#container > div > form > fieldset > div > section > article.find_store_cont > article > article:nth-child(4) > div.loca_step1 > div.loca_step1_cont > ul > li:nth-child(1) > a"
browser.find_element_by_css_selector(seoul_btn).click()

all_btn = "#mCSB_2_container > ul > li:nth-child(1) > a"
browser.find_element_by_css_selector(all_btn).click()
time.sleep(3)
In [8]:
# beautifulsoup 이용, 매장데이터만 파싱
html = browser.page_source
soup = BeautifulSoup(html, 'html.parser')

# 매장 정보만 추출
starbucks_soup_list = soup.select("div.mCSB_container > ul > li.quickResultLstCon")
In [9]:
# 533개의 매장이 존재
len(starbucks_soup_list)
Out[9]:
533
In [10]:
starbucks_soup_list[0]
Out[10]:
<li class="quickResultLstCon" data-code="3762" data-hlytag="null" data-index="0" data-lat="37.501087" data-long="127.043069" data-name="역삼아레나빌딩" data-storecd="1509" style="background:#fff"> <strong>역삼아레나빌딩  <img alt="" class="setStoreFavBtn mCS_img_loaded" data-my_siren_order_store_yn="N" data-name="역삼아레나빌딩" data-store="1509" data-yn="N" src="//image.istarbucks.co.kr/common/img/store/icon_fav_off.png"/></strong> <p class="result_details">서울특별시 강남구 언주로 425 (역삼동)<br/>1522-3232</p> <i class="pin_general">리저브 매장 2번</i></li>
In [11]:
#매장별 데이터프레임 생성
starbucks_list = []
for item in starbucks_soup_list:
    name = item['data-name']
    lat = item['data-lat']
    long = item['data-long']
    store_type = item.select('i')[0]['class'][0][4:]
    address = str(item.select('p.result_details')[0]).split('<br/>')[0].split('>')[1]
    tel = str(item.select('p.result_details')[0]).split('<br/>')[1].split('<')[0]

    starbucks_list.append([name, lat, long, store_type, address, tel])
In [12]:
# 생성된 데이터 프레임, 전화번호는 다 대표전화로 바꿔놓은듯...
columns = ['매장명', '위도', '경도', '매장타입', '주소', '전화번호']

seoul_starbucks_df = pd.DataFrame(data = starbucks_list, columns = columns)
seoul_starbucks_df.head()
Out[12]:
매장명 위도 경도 매장타입 주소 전화번호
0 역삼아레나빌딩 37.501087 127.043069 general 서울특별시 강남구 언주로 425 (역삼동) 1522-3232
1 논현역사거리 37.510178 127.022223 general 서울특별시 강남구 강남대로 538 (논현동) 1522-3232
2 신사역성일빌딩 37.514132 127.020563 general 서울특별시 강남구 강남대로 584 (논현동) 1522-3232
3 국기원사거리 37.499517 127.031495 general 서울특별시 강남구 테헤란로 125 (역삼동) 1522-3232
4 스탈릿대치R 37.494668 127.062583 reserve 서울특별시 강남구 남부순환로 2947 (대치동) 1522-3232
In [13]:
#생성된 데이터 확인
seoul_starbucks_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 533 entries, 0 to 532
Data columns (total 6 columns):
매장명     533 non-null object
위도      533 non-null object
경도      533 non-null object
매장타입    533 non-null object
주소      533 non-null object
전화번호    533 non-null object
dtypes: object(6)
memory usage: 25.1+ KB

1.2 서울열린데이터광장 OPEN API 활용 공공데이터 수집

In [14]:
import requests
import pandas as pd
In [15]:
# api 호출을 위한 함수
def seoul_open_api_data(url, service):
    data_list = None
    try:
        result_dict = requests.get(url).json()
        result_data = result_dict[service]
        code = result_data['RESULT']['CODE']
        if code == 'INFO-000':
            data_list = result_data['row']
    except:
        pass
    return data_list
In [16]:
#시군구 데이터 호출

# 서울열린데이터광장 API key
seoul_api_auth_key = "664f517959647569383372716e6e51"

# get url
url = "http://openapi.seoul.go.kr:8088/{}/json/SdeTlSccoSigW/1/25/".format(seoul_api_auth_key)

sgg_data_list = seoul_open_api_data(url, "SdeTlSccoSigW")
sgg_data_list[0]
Out[16]:
{'OBJECTID': 1.0,
 'SIG_CD': '11320',
 'SIG_KOR_NM': '도봉구',
 'SIG_ENG_NM': 'Dobong-gu',
 'ESRI_PK': 0.0,
 'LAT': '37.6658609',
 'LNG': '127.0317674'}
In [17]:
# 데이터 프레임으로 변환
columns = ['SIG_CD', 'SIG_KOR_NM', 'LAT', 'LNG']
sgg_df = pd.DataFrame(data = sgg_data_list, columns = columns)
sgg_df.head()
Out[17]:
SIG_CD SIG_KOR_NM LAT LNG
0 11320 도봉구 37.6658609 127.0317674
1 11380 은평구 37.6176125 126.9227004
2 11230 동대문구 37.5838012 127.0507003
3 11590 동작구 37.4965037 126.9443073
4 11545 금천구 37.4600969 126.9001546
In [18]:
# 컬럼명 변경
sgg_df.columns = ['시군구코드', '시군구명', '위도', '경도']
sgg_df.head()
Out[18]:
시군구코드 시군구명 위도 경도
0 11320 도봉구 37.6658609 127.0317674
1 11380 은평구 37.6176125 126.9227004
2 11230 동대문구 37.5838012 127.0507003
3 11590 동작구 37.4965037 126.9443073
4 11545 금천구 37.4600969 126.9001546
In [19]:
# 함수를 이용하여 시군구별 인구 데이터 가져오기
pop_url = 'http://openapi.seoul.go.kr:8088/{}/json/octastatapi419/1/26/'.format(seoul_api_auth_key)
pop_data_list = seoul_open_api_data(pop_url, 'octastatapi419')
In [20]:
# API 제공 중단으로 직접 데이터 다운로드
sgg_pop_df = pd.read_csv("report.txt", delimiter= '\t', header = 2, thousands= ',')
sgg_pop_df = sgg_pop_df[1:]
sgg_pop_df_final = sgg_pop_df[['자치구', '계']]
sgg_pop_df_final.columns = ['시군구명', '주민등록인구']
sgg_pop_df_final.head()
Out[20]:
시군구명 주민등록인구
1 종로구 159842
2 중구 135321
3 용산구 244953
4 성동구 302695
5 광진구 361923
In [21]:
# 사업체 데이터 또한 제공 중단으로 직접 다운로드
sgg_biz_df = pd.read_csv("company_report.txt", delimiter= '\t', header = 2, thousands= ',')
sgg_biz_df = sgg_biz_df[sgg_biz_df['동'] == '소계']
sgg_biz_df_final = sgg_biz_df[['자치구', '계', '사업체수']].reset_index(drop = True)
sgg_biz_df_final.columns = ['시군구명', '종사자수', '사업체수']
sgg_biz_df_final.head()
Out[21]:
시군구명 종사자수 사업체수
0 종로구 265017 39952
1 중구 390530 60957
2 용산구 141216 20813
3 성동구 174819 27868
4 광진구 127879 24535

2. 데이터 전처리

In [22]:
# 앞에서 만든 데이터들을 통합

## 스타벅스 주소에서 시군구명 추출
sgg_names = []

for address in seoul_starbucks_df['주소']:
    sgg = address.split()[1]
    sgg_names.append(sgg)
    
seoul_starbucks_df['시군구명'] = sgg_names
seoul_starbucks_df.head()
Out[22]:
매장명 위도 경도 매장타입 주소 전화번호 시군구명
0 역삼아레나빌딩 37.501087 127.043069 general 서울특별시 강남구 언주로 425 (역삼동) 1522-3232 강남구
1 논현역사거리 37.510178 127.022223 general 서울특별시 강남구 강남대로 538 (논현동) 1522-3232 강남구
2 신사역성일빌딩 37.514132 127.020563 general 서울특별시 강남구 강남대로 584 (논현동) 1522-3232 강남구
3 국기원사거리 37.499517 127.031495 general 서울특별시 강남구 테헤란로 125 (역삼동) 1522-3232 강남구
4 스탈릿대치R 37.494668 127.062583 reserve 서울특별시 강남구 남부순환로 2947 (대치동) 1522-3232 강남구
In [23]:
# 시군구 별로 스타벅스 매장 세기
seoul_starbucks_count = seoul_starbucks_df.pivot_table(
    index = '시군구명', 
    values = '매장명', 
    aggfunc = 'count').rename(columns = {'매장명':'스타벅스_매장수'})
seoul_starbucks_count.head()
Out[23]:
스타벅스_매장수
시군구명
강남구 82
강동구 16
강북구 5
강서구 16
관악구 11
In [24]:
# 시군구 목록 데이터에 스타벅스 매장 수 병합
seoul_sgg = pd.merge(sgg_df, seoul_starbucks_count, how = "left", on = '시군구명')
seoul_sgg.head()
Out[24]:
시군구코드 시군구명 위도 경도 스타벅스_매장수
0 11320 도봉구 37.6658609 127.0317674 2
1 11380 은평구 37.6176125 126.9227004 8
2 11230 동대문구 37.5838012 127.0507003 8
3 11590 동작구 37.4965037 126.9443073 11
4 11545 금천구 37.4600969 126.9001546 11
In [25]:
# 시군구별 인구 수 병합
seoul_sgg = pd.merge(seoul_sgg, sgg_pop_df_final, how = "left", on = '시군구명')
seoul_sgg.head()
Out[25]:
시군구코드 시군구명 위도 경도 스타벅스_매장수 주민등록인구
0 11320 도봉구 37.6658609 127.0317674 2 329300
1 11380 은평구 37.6176125 126.9227004 8 485842
2 11230 동대문구 37.5838012 127.0507003 8 358679
3 11590 동작구 37.4965037 126.9443073 11 404617
4 11545 금천구 37.4600969 126.9001546 11 249641
In [26]:
# 시군구별 사업체 종사자 및 사업체 수 병합
seoul_sgg = pd.merge(seoul_sgg, sgg_biz_df_final, how = "left", on = '시군구명')
seoul_sgg.head()
Out[26]:
시군구코드 시군구명 위도 경도 스타벅스_매장수 주민등록인구 종사자수 사업체수
0 11320 도봉구 37.6658609 127.0317674 2 329300 74314 18894
1 11380 은평구 37.6176125 126.9227004 8 485842 90049 24873
2 11230 동대문구 37.5838012 127.0507003 8 358679 140748 31878
3 11590 동작구 37.4965037 126.9443073 11 404617 112564 19916
4 11545 금천구 37.4600969 126.9001546 11 249641 234012 32472

3. 데이터 시각화

3.1 스타벅스 매장분포 시각화

In [27]:
import folium
import json
In [28]:
# 서울 중심으로 folium 지도 생성
starbucks_map = folium.Map(
    location = [37.55875, 126.98745],
    tiles = 'Stamen Terrain',
    zoom_start = 11
)

starbucks_map
Out[28]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [29]:
#서울 스타벅스 위치 서클 마커 그리기
for idx in seoul_starbucks_df.index:
    lat = seoul_starbucks_df.loc[idx, '위도']
    lng = seoul_starbucks_df.loc[idx, '경도']
    
    folium.CircleMarker(
        location=[lat, lng],
        fill = True,
        fill_color = 'green',
        fill_opacity = 1,
        color = 'yellow',
        weight = 1,
        radius = 3
    ).add_to(starbucks_map)

starbucks_map
Out[29]:
Make this Notebook Trusted to load map: File -> Trust Notebook
  • 대체적으로 강남권과 중구, 종로구에 매장이 많이 분포되어 있는 것을 확인할 수 있다
In [30]:
#매장별로 시각화 시도
starbucks_map2 = folium.Map(
    location = [37.55875, 126.98745],
    tiles = 'Stamen Terrain',
    zoom_start = 11
)

for idx in seoul_starbucks_df.index:
    lat = seoul_starbucks_df.loc[idx, '위도']
    lng = seoul_starbucks_df.loc[idx, '경도']
    store_type = seoul_starbucks_df.loc[idx, '매장타입']
    
    fillColor = ''
    if store_type == 'general':
        fillColor = 'gray'
        size = 1
    elif store_type == 'reserve':
        fillColor = 'blue'
        size = 5
    elif store_type == 'generalDT':
        fillColor = 'red'
        size = 5
    
    folium.CircleMarker(
        location=[lat, lng],
        fill = True,
        fill_color = fillColor,
        fill_opacity = 1,
        color = 'yellow',
        weight = 1,
        radius = size
    ).add_to(starbucks_map2)

starbucks_map2
Out[30]:
Make this Notebook Trusted to load map: File -> Trust Notebook
  • 리저브는 주로 강남과 종로 등 서울 중심지에 위치해 있고, 드라이브스루는 주로 외곽에 위치해 있다

3.2 시군구별 스타벅스 매장 수 시각화

In [31]:
# 서울시 시군구 행정 경계 지도 불러오기
sgg_geojson_file_path = "./maps/seoul_sgg.geojson"
seoul_sgg_geo = json.load(open(sgg_geojson_file_path, encoding='utf-8'))
seoul_sgg_geo['features'][0]['properties']
Out[31]:
{'SIG_CD': '11320',
 'SIG_KOR_NM': '도봉구',
 'SIG_ENG_NM': 'Dobong-gu',
 'ESRI_PK': 0,
 'SHAPE_AREA': 0.00210990544544,
 'SHAPE_LEN': 0.239901251347}
In [32]:
starbucks_bubble = folium.Map(
    location = [37.55875, 126.98745],
    tiles = 'CartoDB dark_matter',
    zoom_start = 11
)
In [33]:
def style_function(feature):
    return {
        'opacity' : 0.7,
        'weight' : 1,
        'color' : 'white',
        'fillOpacity' : 0,
        'dashArray': '5, 5',
    }

folium.GeoJson(
    seoul_sgg_geo,
    style_function = style_function
).add_to(starbucks_bubble)

starbucks_bubble
Out[33]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [34]:
# 시군구별 매장 수와, 평균 매장 수와 비교에서 더 많은지 큰지도 색깔로 표시
starbucks_mean = seoul_sgg['스타벅스_매장수'].mean()
print(starbucks_mean)
21.32
In [35]:
for idx in seoul_sgg.index:
    lat = seoul_sgg.loc[idx, '위도']
    lng = seoul_sgg.loc[idx, '경도']
    count = seoul_sgg.loc[idx, '스타벅스_매장수']
    
    if count > starbucks_mean:
        fillColor = '#FF0000'
    else:
        fillColor = '#CCFF33'
        
    folium.CircleMarker(
        location=[lat, lng],
        fill_color = fillColor,
        fill_opacity = 0.7,
        color = '#FFFF00',
        weight = 1.5,
        radius = count/2
    ).add_to(starbucks_bubble)
    
starbucks_bubble
Out[35]:
Make this Notebook Trusted to load map: File -> Trust Notebook
  • 강남구, 서초구, 송파구와 중구, 종로구, 영등포구, 마포구, 서대문구에 스타벅스 매장이 많다
In [36]:
# 같은 데이터를 단계구분도로 시각화
starbucks_choropleth = folium.Map(
    location = [37.55875, 126.98745],
    tiles = 'CartoDB dark_matter',
    zoom_start = 11
)

folium.Choropleth(
    geo_data = seoul_sgg_geo,
    data = seoul_sgg,
    columns = ['시군구명', '스타벅스_매장수'],
    fill_color= 'YlGn',
    fill_opacity= 0.7,
    line_opacity= 0.5,
    key_on = 'properties.SIG_KOR_NM'
).add_to(starbucks_choropleth)

starbucks_choropleth
Out[36]:
Make this Notebook Trusted to load map: File -> Trust Notebook

3.3 스타벅스 매장 수와 인구수 비교

In [37]:
# 시군구별 인구 수의 단계분포도 위에 스타벅스 매장 수의 버블지도를 그려봅니다
starbucks_choropleth = folium.Map(
    location = [37.55875, 126.98745],
    tiles = 'CartoDB dark_matter',
    zoom_start = 11
)

folium.Choropleth(
    geo_data = seoul_sgg_geo,
    data = seoul_sgg,
    columns = ['시군구명', '주민등록인구'],
    fill_color = 'YlGn',
    fill_opacity = 0.7,
    line_opacity = 0.5,
    key_on = 'properties.SIG_KOR_NM'
).add_to(starbucks_choropleth)

for idx in seoul_sgg.index:
    lat = seoul_sgg.loc[idx, '위도']
    lng = seoul_sgg.loc[idx, '경도']
    count = seoul_sgg.loc[idx, '스타벅스_매장수']
    
    if count > starbucks_mean:
        fillColor = '#FF0000'
    else:
        fillColor = '#CCFF33'
        
    folium.CircleMarker(
        location=[lat, lng],
        fill_color = fillColor,
        fill_opacity = 0.7,
        color = '#FFFF00',
        weight = 1.5,
        radius = count/2
    ).add_to(starbucks_choropleth)

starbucks_choropleth
Out[37]:
Make this Notebook Trusted to load map: File -> Trust Notebook
  • 시군구의 인구수와 스타벅스 매장수의 분포가 높은 상관관계를 보이지 않습니다. 중구, 종로구에 매장이 많은것에 비해 인구수가 매우 적고, 강서구나 관악구와 같이 인구수가 높은 곳에 매장수가 적습니다
In [38]:
# 인구 1만명당 스타벅스 매장수
seoul_sgg['만명당_매장수'] = seoul_sgg['스타벅스_매장수'] / (seoul_sgg['주민등록인구'] / 10000)
In [39]:
# 1만명당 매장 수가 상위 10%인 시군구만 빨간색으로
viz_map_1 = folium.Map(
    location = [37.55875, 126.98745],
    tiles = 'CartoDB dark_matter',
    zoom_start = 11
)

def style_function(feature):
    return {
        'opacity' : 0.7,
        'weight' : 1,
        'color' : 'white',
        'fillOpacity' : 0,
        'dashArray': '5, 5',
    }

folium.GeoJson(
    seoul_sgg_geo,
    style_function = style_function
).add_to(viz_map_1)

top = seoul_sgg['만명당_매장수'].quantile(0.9)

for idx in seoul_sgg.index:
    lat = seoul_sgg.loc[idx, '위도']
    lng = seoul_sgg.loc[idx, '경도']
    r = seoul_sgg.loc[idx, '만명당_매장수']
    
    if r > top:
        fillColor = '#FF0000'
    else:
        fillColor = '#CCFF33'
        
    folium.CircleMarker(
        location=[lat, lng],
        fill_color = fillColor,
        fill_opacity = 0.7,
        color = '#FFFF00',
        weight = 1.5,
        radius = r * 10
    ).add_to(viz_map_1)

viz_map_1
Out[39]:
Make this Notebook Trusted to load map: File -> Trust Notebook
  • 중구, 종로구가 적은 인구에도 매장수가 가장 많은것으로 표시된다. 첫 번째 가설인 "거주 인구가 많은 지역에 스타벅스 매장이 많다"는 틀린 것으로 보입니다

3.4 스타벅스 매장 수와 사업체 수 비교

In [40]:
# 시군구별 사업체 종사자 수의 단계분포도 위에 스타벅스 매장 수의 버블지도를 그려봅니다
starbucks_choropleth = folium.Map(
    location = [37.55875, 126.98745],
    tiles = 'CartoDB dark_matter',
    zoom_start = 11
)

folium.Choropleth(
    geo_data = seoul_sgg_geo,
    data = seoul_sgg,
    columns = ['시군구명', '종사자수'],
    fill_color = 'YlGn',
    fill_opacity = 0.7,
    line_opacity = 0.5,
    key_on = 'properties.SIG_KOR_NM'
).add_to(starbucks_choropleth)

for idx in seoul_sgg.index:
    lat = seoul_sgg.loc[idx, '위도']
    lng = seoul_sgg.loc[idx, '경도']
    count = seoul_sgg.loc[idx, '스타벅스_매장수']
    
    if count > starbucks_mean:
        fillColor = '#FF0000'
    else:
        fillColor = '#CCFF33'
        
    folium.CircleMarker(
        location=[lat, lng],
        fill_color = fillColor,
        fill_opacity = 0.7,
        color = '#FFFF00',
        weight = 1.5,
        radius = count/2
    ).add_to(starbucks_choropleth)

starbucks_choropleth
Out[40]:
Make this Notebook Trusted to load map: File -> Trust Notebook
  • 강남구, 서초구, 송파구, 종로구, 중구 등 사업체 종사자(직장인)의 인구 수 많은 곳에 스타벅스 매장 수가 평균 이상인 곳이 다수 포진 되어있다. 직장인이 많은 곳에 스타벅스 매장이 많다는 가설은 어느정도 유의미한 가설이다
In [41]:
# 인구 1만명당 스타벅스 매장수
seoul_sgg['종사자_만명당_매장수'] = seoul_sgg['스타벅스_매장수'] / (seoul_sgg['종사자수'] / 10000)

viz_map_2 = folium.Map(
    location = [37.55875, 126.98745],
    tiles = 'CartoDB dark_matter',
    zoom_start = 11
)

def style_function(feature):
    return {
        'opacity' : 0.7,
        'weight' : 1,
        'color' : 'white',
        'fillOpacity' : 0,
        'dashArray': '5, 5',
    }

folium.GeoJson(
    seoul_sgg_geo,
    style_function = style_function
).add_to(viz_map_2)

top = seoul_sgg['종사자_만명당_매장수'].quantile(0.9)

for idx in seoul_sgg.index:
    lat = seoul_sgg.loc[idx, '위도']
    lng = seoul_sgg.loc[idx, '경도']
    r = seoul_sgg.loc[idx, '종사자_만명당_매장수']
    
    if r > top:
        fillColor = '#FF0000'
    else:
        fillColor = '#CCFF33'
        
    folium.CircleMarker(
        location=[lat, lng],
        fill_color = fillColor,
        fill_opacity = 0.7,
        color = '#FFFF00',
        weight = 1.5,
        radius = r * 10
    ).add_to(viz_map_2)

viz_map_2
Out[41]:
Make this Notebook Trusted to load map: File -> Trust Notebook
  • 직장인 만명 당 스타벅스 매장 수를 보았을때, 어느정도 평준화 되는 모습을 보면, 직장이 많을 수록 스타벅스 매장도 많아 진다고 볼 수 있다
In [ ]: